const chokidar = require("chokidar");
const chalk = require("chalk");
const bodyParser = require("body-parser");
const assert = require("assert");
const pathToRegexp = require("path-to-regexp");
const path = require("path");
const multer = require("multer");
const slash = require("slash2");
const glob = require("glob");

const debug = require("debug")("xMock");
const VALID_METHODS = ["get", "post", "put", "patch", "delete"];
const BODY_PARSED_METHODS = ["post", "put", "patch", "delete"];

const mockBaseUrl = '/mock';
const mockFolder = ".mock-server/mock";
const mockSourceData = ".mock-server/sources";
const mockDir = path.join(process.cwd(), mockFolder);
const mockStaticDir = path.join(process.cwd(), mockSourceData);
const cwd = process.cwd();

function noop() {
}
function createHandler(method, path, handle) {
  return function(req, res, next) {
    if (BODY_PARSED_METHODS.includes(method)) {
      bodyParser.json({ limit: "5mb", strict: false })(req, res, () => {
        bodyParser.urlencoded({ limit: "5mb", extended: true })(req, res, () => {
          sendData();
        });
      });
    } else {
      sendData();
    }

    function sendData() {
      if (typeof handle === "function") {
        multer().any()(req, res, () => {
          handle(req, res, next);
        });
      } else {
        res.json(handle);
      }
    }
  };
}

function getMockFiles(opts) {
  const { cwd, config = {} } = opts;
  const mockFolder = (config.mock || {}).src || "mock";
  const absMockPath = slash(path.join(cwd, mockFolder));
  let mockFiles = glob
    .sync(`${mockFolder}/**/*.js`, {
      cwd,
      ignore: (config.mock || {}).exclude || []
    })
    .map(p => path.join(cwd, p));

  // 处理一下路径，不然在 win 下面会报错
  mockFiles = mockFiles.map(p => slash(p));
  debug(`load mock data from ${absMockPath}, including files ${JSON.stringify(mockFiles)}`);
  return mockFiles;
}

function getMockConfigFromFiles(files) {
  return files.reduce((memo, mockFile) => {
    try {
      const m = require(mockFile); // eslint-disable-line
      const newMemo = m.default || m;
      if (newMemo && Array.isArray(newMemo) && newMemo.length>0) {
        memo = [
          ...memo,
          ...newMemo
        ];
      }
      return memo;
    } catch (e) {
      throw new Error(e.stack);
    }
  }, []);
}

function getMockConfig(opts) {
  const files = getMockFiles(opts);
  return getMockConfigFromFiles(files);
}

function normalizeConfig(routeMap) {
  return routeMap.reduce((routes, route) => {
    const typeRes = typeof route.handle;
    assert(
      typeRes === "function" || typeRes === "object",
      `mock value of ${route} should be function or object, but got ${typeRes}`
    );
    route.path = `${mockBaseUrl}${route.path}`;
    const { method, path } = route;
    assert(
      VALID_METHODS.includes(method),
      `Invalid method ${method} for path ${path}, please check your mock files.`
    );
    const keys = [];
    const reg = pathToRegexp(path, keys);
    if (method && path) {
      routes.push({
        method,
        path,
        reg,
        keys,
        handle: createHandler(method, path, route.handle)
      });
    }
    return routes;
  }, []);
}

function registerRoutes(app) {
  const opts = {
    cwd,
    config: {
      mock: {
        src: mockFolder,
        exclude: [`${mockSourceData}/**`]
      }
    }
  };
  const { onError = noop } = opts;
  let mocksData = [];
  try {
    mocksData = normalizeConfig(getMockConfig(opts));
  } catch (e) {
    onError(e);
    console.log(chalk.black.bgRed.bold(" Error "), chalk.green(" Mock files parse failed"));
  }

  for (const mock of mocksData) {
    app[mock.method](mock.path, mock.handle);
  }

  const mockLastIndex = app._router.stack.length;
  const mockRoutesLength = Object.keys(mocksData).length;
  return {
    mockRoutesLength: mockRoutesLength,
    mockStartIndex: mockLastIndex - mockRoutesLength
  };
}

function unregisterRoutes() {
  Object.keys(require.cache).forEach(i => {
    if (i.includes(mockDir)||i.includes(mockStaticDir)) {
      debug('Remove cache', i);
      delete require.cache[require.resolve(i)];
    }
  });
}

module.exports = app => {
  // es6 polyfill
  require("@babel/register");

  // parse app.body
  // https://expressjs.com/en/4x/api.html#req.body
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({
    extended: true
  }));

  const mockRoutes = registerRoutes(app);
  let mockRoutesLength = mockRoutes.mockRoutesLength;
  let mockStartIndex = mockRoutes.mockStartIndex;

  // watch files, hot reload mock server
  chokidar.watch(mockFolder, {
    ignored: [
      'index.js',
      'preview.js'
    ],
    ignoreInitial: true
  }).on("all", (event, path) => {
    if (event === "change" || event === "add") {
      try {
        // remove mock routes stack
        app._router.stack.splice(mockStartIndex, mockRoutesLength);
        // clear routes cache
        unregisterRoutes();

        const mockRoutes = registerRoutes(app);
        mockRoutesLength = mockRoutes.mockRoutesLength;
        mockStartIndex = mockRoutes.mockStartIndex;

        console.log(chalk.black.bgGreen(" Mock Server hot reload "), chalk.magentaBright(`Changed: ${path}`));
      } catch (error) {
        console.log(chalk.redBright(error));
      }
    }
  });
};
